﻿using System;
using System.Configuration;
using System.IO;
using MLSharp.Classification;
using MLSharp.Utilities;

namespace MLSharp.SupportVectorMachines
{
	/// <summary>
	/// Trains a classifier using the command-line, C-based
	/// libSVM tool.  This may be faster than the C# API.
	/// </summary>
	public class LibSvmExClassifierFactory : IClassifierFactory
	{
		#region Logger

		/// <summary>
		/// The logger for the class.
		/// </summary>
		private static readonly log4net.ILog mLogger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The working directory where temporary files should be stored.
		/// </summary>
		private string mWorkingDirectory;

		/// <summary>
		/// The path to the libSVM train tool
		/// </summary>
		private string mLibSvmTrainPath;

		/// <summary>
		/// The path to the libSVM predict tool.
		/// </summary>
		private readonly string mLibSvmPredictPath;

		#endregion

		#region Public Properties

		/// <summary>
		/// The options to use when training a new classifier.
		/// </summary>
		public LibSvmExOptions Options { get; private set; }

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a classifier factory using path information from the 
		/// App.config file.
		/// </summary>
		public LibSvmExClassifierFactory()
		{
			mWorkingDirectory = ConfigurationManager.AppSettings["LibSvm.WorkingDirectory"] ?? @".\LibSvm";

			mLibSvmTrainPath = ConfigurationManager.AppSettings["LibSvm.LibSvmTrainPath"];

			mLibSvmPredictPath = ConfigurationManager.AppSettings["LibSvm.LibSvmPredictPath"];

			Initialize();
		}

		/// <summary>
		/// Creates a classifier factory that will use the specified settings instead
		/// of retrieving them from AppSettings.
		/// </summary>
		/// <param name="workingDirectory"></param>
		/// <param name="libSvmTrainPath"></param>
		/// <param name="libSvmPredictPath"></param>
		public LibSvmExClassifierFactory(string workingDirectory, string libSvmTrainPath, string libSvmPredictPath)
		{
			mWorkingDirectory = workingDirectory;

			mLibSvmTrainPath = libSvmTrainPath;

			mLibSvmPredictPath = libSvmPredictPath;

			Initialize();
		}


		#endregion

		#region Private Methods

		/// <summary>
		/// Performs any necessary setup (for now, it just creates the working directory if it doesn't exist).
		/// </summary>
		private void Initialize()
		{
			Options = new LibSvmExOptions();

			mWorkingDirectory = Path.GetFullPath(mWorkingDirectory);

			if (!Directory.Exists(mWorkingDirectory))
			{
				Directory.CreateDirectory(mWorkingDirectory);
			}

			if (string.IsNullOrEmpty(mLibSvmTrainPath))
			{
				throw new InvalidOperationException("No path specified for svm-train.exe.");
			}

			mLibSvmTrainPath = Path.GetFullPath(mLibSvmTrainPath);

			//If the training program can't be found, explode.
			if (!File.Exists(mLibSvmTrainPath))
			{
				throw new InvalidOperationException("Unable to find libSVM at " + Path.GetFullPath(mLibSvmTrainPath));
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Gets a libsvm support vector machine trained from the specified data.
		/// </summary>
		/// <param name="trainingData">The training data to use.</param>
		/// <returns>The SVM.</returns>
		public LibSvmExClassifier BuildSupportVectorMachine(IDataSet trainingData)
		{
			//Step 1: Write the training data to a temp file.
			string trainingFileName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".data");
			string modelFileName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".model");

			LibSvmDataWriter.WriteData(trainingData, trainingFileName);

			//Step 2: Run libSVM
			ProcessOutput output = ProcessWrapper.Run(mLibSvmTrainPath, mWorkingDirectory,
			                                   string.Format("{0} \"{1}\" \"{2}\"",
			                                                 Options, trainingFileName, modelFileName));

			//Clean up the training data
			File.Delete(trainingFileName);

			//Step 3: Verify that the model file exists
			if (!File.Exists(modelFileName))
			{
				//If not, log the output of libSVM and throw an exception
				if (mLogger.IsErrorEnabled)
				{
					mLogger.ErrorFormat("libSVM did not create a model file.  Dumping libSVM output: \r\n{0}",
					                    output);
				}

				throw new InvalidOperationException("libSVM did not create a model file.");
			}

			//If so, cram a reference to the file in LibSvmExClassifier and return it.
			//This temp file will be cleaned up by LibSvmClassifierFactory.
			return new LibSvmExClassifier(modelFileName, mLibSvmPredictPath, mWorkingDirectory);
		}

		#endregion

		#region Implementation of IClassifierFactory

		/// <summary>
		/// Builds a classifier to predict the specified attribute using the 
		/// specified training data.
		/// </summary>
		/// <param name="trainingData">The training data.</param>
		/// <returns>A classifier.</returns>
		IClassifier IClassifierFactory.BuildClassifier(IDataSet trainingData)
		{
			return BuildSupportVectorMachine(trainingData);
		}

		/// <summary>
		/// The options object for the factory.
		/// </summary>
		/// <remarks>
		/// Implementors should implement this property explicitly and expose 
		/// a strongly-typed options object instead.  
		/// </remarks>
		object IClassifierFactory.Options
		{
			get { return Options; }
		}

		#endregion
	}
}
